查看原文
其他

Flutter混编工程之Font桥接

徐宜生 群英传 2022-12-21

点击上方蓝字关注我,知识会给你力量


在混编开发中,我们经常遇到要全局替换当前字体的需求,在Native开发中,我们通常会加载Asset或者下载的字体文件,那么在Flutter中,如何直接使用Native的字体文件呢?

毕竟大部分的字体文件都毕竟大,特别是一些字体还有加密策略,如果在Flutter中再创建一份字体文件,既浪费空间,而且也是一种重复代码,所以,我们需要在Flutter端,获取Native的字体文件。

在Flutter中,系统给我们提供了FontLoader,来动态加载字体,与前面的做法一样,我们创建一个Native接口,来获取Native传来的Byte数据流,并借助FontLoader来加载字体。

FontLoader加载字体数据

为了提高传输的效率,我们使用BasicMessageChannel来作为Channel的实现,这些在我们讲解Flutter与Native的通信机制中,都已经演示过了,我们直接拿来Google的Demo代码,修改下需要的内容,将FontLoader引入,代码如下所示。

import 'dart:async';
import 'dart:convert';

import 'package:flutter/services.dart';

class NativeFontApi {
  NativeFontApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger;

  final BinaryMessenger? _binaryMessenger;

  Future<void> loadNativeFont(String fontFamily) async {
    final BasicMessageChannel<ByteData> channel = BasicMessageChannel<ByteData>(
      'dev.flutter.pigeon.NativeFontApi.loadNativeFont',
      const BinaryCodec(),
      binaryMessenger: _binaryMessenger,
    );
    try {
      final uInt8List = utf8.encoder.convert(fontFamily);
      final Future<ByteData> fontData = _loadFontFileByteData(uInt8List.buffer.asByteData(), channel);
      if (await fontData != ByteData(0)) {
        final FontLoader fontLoader = FontLoader(fontFamily);
        fontLoader.addFont(fontData);
        fontLoader.load();
      }
    } catch (e) {
      return;
    }
  }

  Future<ByteData> _loadFontFileByteData(ByteData data, BasicMessageChannel channel) async {
    final ByteData? fontData = await channel.send(data);
    if (fontData != null) {
      return fontData;
    } else {
      return ByteData(0);
    }
  }
}

在加载好字体数据之后,我们在代码中就可以直接使用看,这和在配置文件中新增字体后的使用方式一样,直接指定fontFamily即可,代码如下所示。

Text(
  model[index].bookName ?? "",
  style: const TextStyle(
    fontSize: 16,
    fontFamily: 'xxx_Medium_60',
  ),
)

唯一需要注意的是,我们需要在程序启动时,初始化我们的字体文件,代码如下所示,通过loadNativeFont调用Channel来加载字体文件。

NativeFontApi().loadNativeFont('xxx_Medium_60');

Native实现

我们仿照pigeon的实现方式,来创建自己的FontBridgeApi,之所以没通过pigeon直接生成,那是因为pigeon还不支持生成Byte数组的方式,所以我们只能自己来写,代码如下所示。

// Autogenerated from Pigeon (v1.0.15), do not edit directly.
// See also: https://pub.dev/packages/pigeon

import java.nio.ByteBuffer;

import io.flutter.plugin.common.BasicMessageChannel;
import io.flutter.plugin.common.BinaryCodec;
import io.flutter.plugin.common.BinaryMessenger;

@SuppressWarnings({"unused""unchecked""CodeBlock2Expr""RedundantSuppression"})
public class FontBridgeApi {

    public interface NativeFontApi {
        ByteBuffer loadNativeFont(String familyName);

        static void setup(BinaryMessenger binaryMessenger, NativeFontApi api) {
            BasicMessageChannel<ByteBuffer> channel =
                    new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeFontApi.loadNativeFont", BinaryCodec.INSTANCE);
            if (api != null) {
                channel.setMessageHandler((message, reply) -> {
                    try {
                        String param = new String(message != null ? message.array() : new byte[0]);
                        ByteBuffer output = api.loadNativeFont(param);
                        reply.reply(output);
                    } catch (Error | RuntimeException ignored) {
                    }
                });
            } else {
                channel.setMessageHandler(null);
            }
        }
    }
}

下面就是实现接口,代码如下所示。

private class NativeFontApiImp : FontBridgeApi.NativeFontApi {
    private val externalAppFontIndexes = intArrayOf(
        ETConverter.FONT_TYPE_INDEX_XXX_LIGHT, 
        ETConverter.FONT_TYPE_INDEX_XXX_MEDIUM,
        ETConverter.FONT_TYPE_INDEX_XXXXX 
    )

    override fun loadNativeFont(familyName: String?): ByteBuffer {
        val trueTypeFolder = File(ApplicationContext.getInstance().filesDir, ETConverter.FOLDER_TRUE_TYPE_FONTS)
        if (!trueTypeFolder.exists()) trueTypeFolder.mkdirs()
        val familyNameIndex = when (familyName) {
            "XXX_Light" -> 0
            "XXX_Medium_60" -> 1
            "XXXXSerif_Bold" -> 2
            else -> 0
        }
        val ttf = File(trueTypeFolder, ETConverter.getFontTypeName(externalAppFontIndexes[familyNameIndex]) + ETConverter.POSTFIX_NEW_TTF)
        val inputStream: InputStream = FileInputStream(ttf)
        val output = ByteArrayOutputStream()
        val buffer = ByteArray(4096)
        var n = 0
        while (true) {
            try {
                if (-1 == inputStream.read(buffer).also { n = it }) {
                    inputStream.close()
                    output.close()
                    break
                }
            } catch (e: IOException) {
                e.printStackTrace()
            }
            output.write(buffer, 0, n)
        }
        return ByteBuffer.allocateDirect(output.toByteArray().size).put(output.toByteArray())
    }
}

我们放到前面pigeon统一实现的地方进行初始化,代码如下。

FontBridgeApi.NativeFontApi.setup(flutterEngine.dartExecutor, NativeFontApiImp())

优化

通过上面的方式,我们很轻松就实现了Flutter端加载Native的字体文件,但是在代码实现过程中,实际上有些地方是可以进行优化的,例如在Flutter中加载字体的异步方法中,我们可以构建一个枚举,根据不同的状态值,来修改代码的执行逻辑,例如增加:「加载中」、「加载失败」等状态,这样在程序异常的时候,可以判断是否需要跳过后面的加载流程、或者是重新执行加载流程,可以增加代码的鲁棒性。

// 状态
enum NativeFontLoadState { loading, failed, complete, notFound }
// 返回枚举状态
Future<NativeFontLoadState> loadFontIfNeeded(String fontFamily)

全局字体

在Flutter中,我们通常会根据自己项目的特点,封装一些Text组件,那么在这些组件中,就可以直接指定fontFamily,这样在业务开发时,就不需要重复指定fontFamily了,直接使用XXXText即可。

除了这种方式以外,还可以在APP的themeData中,直接指定fontFamily,代码如下:

theme: ThemeData(
    fontFamily: xxxx,
),

这样可以为子组件提供默认的字体支持。如果在某些场景下需要修改默认字体,那么重新给Text设置不同的fontFamily覆盖即可。

向大家推荐下我的网站 https://xuyisheng.top/  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问



往期推荐


本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。
< END >
作者:徐宜生

更文不易,点个“三连”支持一下👇


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存